package com.rollway.restful.support;

import com.rollway.restful.MethodMetadata;
import com.rollway.restful.annotation.SingleOptions;
import com.rollway.restful.options.CompositeRestfulOptions;
import com.rollway.restful.options.RestfulOptions;
import com.rollway.restful.processor.AnnotatedParameterContext;
import com.rollway.restful.processor.AnnotatedParameterProcessor;
import com.rollway.restful.processor.PathVariableParameterProcessor;
import com.rollway.restful.processor.RequestBodyParameterProcessor;
import com.rollway.restful.processor.RequestHeaderParameterProcessor;
import com.rollway.restful.processor.RequestParamParameterProcessor;
import com.rollway.restful.processor.RequestPartParameterProcessor;
import com.rollway.restful.utils.Types;
import com.rollway.restful.utils.Util;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.KotlinDetector;
import org.springframework.core.KotlinReflectionParameterNameDiscoverer;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.PrioritizedParameterNameDiscoverer;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static com.rollway.restful.utils.Util.checkState;
import static com.rollway.restful.utils.Util.emptyToNull;
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;

public class MethodMetadataParser implements ResourceLoaderAware, EnvironmentAware {
    ResourceLoader resourceLoader;
    CompositeRestfulOptions compositeRestFulOptions;
    Environment environment;

    public MethodMetadataParser(CompositeRestfulOptions compositeRestFulOptions) {
        this.compositeRestFulOptions = compositeRestFulOptions;
    }

    public MethodMetadataParser() {
    }

    private final Map<String, Method> processedMethods = new HashMap<>();
    DefaultParameterNameDiscoverer DEFAULT_DISCOVERER = new DefaultParameterNameDiscoverer();
    Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;

    {
        List<AnnotatedParameterProcessor> processors = new ArrayList<>();
        processors.add(new PathVariableParameterProcessor());
        processors.add(new RequestParamParameterProcessor());
        processors.add(new RequestHeaderParameterProcessor());
        processors.add(new RequestPartParameterProcessor());
        processors.add(new RequestBodyParameterProcessor());
        annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
    }

    public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType, RestfulOptions serviceOptions) {
        checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
                targetType.getSimpleName());
        checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
                targetType.getSimpleName());
        if (targetType.getInterfaces().length == 1) {
            checkState(targetType.getInterfaces()[0].getInterfaces().length == 0,
                    "Only single-level inheritance supported: %s",
                    targetType.getSimpleName());
        }
        final Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
        for (final Method method : targetType.getMethods()) {
            if (method.getDeclaringClass() == Object.class ||
                    (method.getModifiers() & Modifier.STATIC) != 0 ||
                    Util.isDefault(method)) {
                continue;
            }
            MethodMetadata metadata = parseAndValidateMetadata(targetType, method, serviceOptions);
            checkState(!result.containsKey(metadata.getMethodKey()), "Overrides unsupported: %s",
                    metadata.getMethodKey());
            result.put(metadata.getMethodKey(), metadata);
        }
        return new ArrayList<>(result.values());
    }

    private MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method, RestfulOptions serviceOptions) {
        String methodKey = Util.configKey(targetType, method);
        processedMethods.put(methodKey, method);
        MethodMetadata data = new MethodMetadata();
        data.setServiceName(serviceOptions.getServiceName());
        data.setTargetType(targetType);
        data.setMethod(method);
        data.setReturnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
        data.setMethodKey(methodKey);
        RestfulOptions methodOptions = serviceOptions.copy();
        methodOptions.setMethodKey(data.getMethodKey());
        processAnnotationOnMethod(data, method, methodOptions);
        checkState(data.getHttpMethod() != null,
                "Method %s not  annotated with HTTP method type (ex. GET, POST)", data.getMethodKey());
        compositeRestFulOptions.get(data.getServiceName()).put(methodKey, methodOptions);
        final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        final int count = parameterAnnotations.length;
        for (int i = 0; i < count; i++) {
            if (parameterAnnotations[i] != null) {
                processAnnotationsOnParameter(data, parameterAnnotations[i], i);
            }
        }
        return data;
    }

    protected void processAnnotationOnMethod(MethodMetadata data, Method method, RestfulOptions methodOptions) {
        Annotation[] methodAnnotations = method.getAnnotations();
        for (Annotation methodAnnotation : methodAnnotations) {
            processRequestMapping(data, method, methodOptions, methodAnnotation);
            processSingleOptions(data, methodOptions, methodAnnotation);
        }
    }

    private void processSingleOptions(MethodMetadata data, RestfulOptions methodOptions, Annotation methodAnnotation) {
        if (SingleOptions.class.isInstance(methodAnnotation)) {
            SingleOptions annotation = (SingleOptions) methodAnnotation;
            Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(annotation);
            attributes.entrySet().stream()
                    .filter(e -> Util.isValid(e.getValue()))
                    .forEach(entry -> {
                        RestfulOptions.Options options = methodOptions.getOrInitOptions();
                        Util.fillFields(environment, options, entry);
                    });

        }
    }

    private void processRequestMapping(MethodMetadata data, Method method, RestfulOptions methodOptions, Annotation methodAnnotation) {
        if (RequestMapping.class.isInstance(methodAnnotation)
                || methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {
            RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
            RequestMethod[] methods = methodMapping.method();
            if (methods.length == 0) {
                methods = new RequestMethod[]{RequestMethod.GET};
            }
            checkOne(method, methods, "httpMethod");
            data.setHttpMethod(HttpMethod.valueOf(methods[0].name()));
            if (methodMapping.value().length > 0) {
                String pathValue = (String) Util.resolve(environment, methodMapping.value()[0]);
                pathValue = Util.valNoNullWithSep(pathValue);
                methodOptions.setPath(pathValue);
            }
            // produces
            parseProduces(data, method, methodMapping);
            // consumes
            parseConsumes(data, method, methodMapping);
        }
    }

    private void parseProduces(MethodMetadata md, Method method, RequestMapping annotation) {
        String[] serverProduces = annotation.produces();
        String clientAccepts = serverProduces.length == 0 ? null : emptyToNull(serverProduces[0]);
        if (clientAccepts != null) {
            md.getHeaders().put(HttpHeaders.ACCEPT, clientAccepts);
        }
    }

    private void parseConsumes(MethodMetadata md, Method method, RequestMapping annotation) {
        String[] serverConsumes = annotation.consumes();
        String clientProduces = serverConsumes.length == 0 ? null : emptyToNull(serverConsumes[0]);
        if (clientProduces != null) {
            md.getHeaders().put(HttpHeaders.CONTENT_TYPE, clientProduces);
        }
    }


    private Map<Class<? extends Annotation>, AnnotatedParameterProcessor> toAnnotatedArgumentProcessorMap(
            List<AnnotatedParameterProcessor> processors) {
        Map<Class<? extends Annotation>, AnnotatedParameterProcessor> result = new HashMap<>();
        for (AnnotatedParameterProcessor processor : processors) {
            result.put(processor.getAnnotationType(), processor);
        }
        return result;
    }

    private void processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
        AnnotatedParameterContext.SimpleAnnotatedParameterContext context = new AnnotatedParameterContext.SimpleAnnotatedParameterContext(data, paramIndex);
        Method method = processedMethods.get(data.getMethodKey());
        for (Annotation parameterAnnotation : annotations) {
            AnnotatedParameterProcessor processor = annotatedArgumentProcessors
                    .get(parameterAnnotation.annotationType());
            if (processor != null) {
                Annotation processParameterAnnotation;
                processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue(parameterAnnotation,
                        method, paramIndex);
                processor.processArgument(context, processParameterAnnotation, method);
                return;
            }
        }
        throw new IllegalStateException("No annotation found at index " + paramIndex + " for " + data.getMethodKey());
    }

    private Annotation synthesizeWithMethodParameterNameAsFallbackValue(Annotation parameterAnnotation, Method method,
                                                                        int parameterIndex) {
        Map<String, Object> annotationAttributes = AnnotationUtils.getAnnotationAttributes(parameterAnnotation);
        Object defaultValue = AnnotationUtils.getDefaultValue(parameterAnnotation);
        if (defaultValue instanceof String && defaultValue.equals(annotationAttributes.get(AnnotationUtils.VALUE))) {
            Type[] parameterTypes = method.getGenericParameterTypes();
            String[] parameterNames = DEFAULT_DISCOVERER.getParameterNames(method);
            if (shouldAddParameterName(parameterIndex, parameterTypes, parameterNames)) {
                annotationAttributes.put(AnnotationUtils.VALUE, parameterNames[parameterIndex]);
            }
        }
        return AnnotationUtils.synthesizeAnnotation(annotationAttributes, parameterAnnotation.annotationType(), null);
    }

    private boolean shouldAddParameterName(int parameterIndex, Type[] parameterTypes, String[] parameterNames) {
        // has a parameter name
        return parameterNames != null && parameterNames.length > parameterIndex
                // has a type
                && parameterTypes != null && parameterTypes.length > parameterIndex;
    }

    private void checkOne(Method method, Object[] values, String field) {
        Util.checkState(values != null && values.length == 1, "Method %s can only contain 1 %s field. Found: %s",
                method.getName(), values == null ? null : Arrays.asList(values));
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }


    class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
        boolean imageCode = System.getProperty("org.graalvm.nativeimage.imagecode") == null;

        public DefaultParameterNameDiscoverer() {
            if (KotlinDetector.isKotlinReflectPresent() && imageCode ) {
                this.addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
            }
            this.addDiscoverer(new StandardReflectionParameterNameDiscoverer());
            this.addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
        }
    }
}
